# Samba tune parser

def word_split(l):
    "Split a line into words"
    return [w for w in l.split(' ') if w != '']

class ParseError:
    "Parsing error"
    def __init__(self, line_number, error):
        self.line_number = line_number
        self.error = error
    def __repr__(self):
        return "ParseError(%d, '%s')" % (self.line_number, self.error)

class Line:
    "Single music line"
    def __init__(self, bar_length, line_number, line):
        "Parse a line"
        self.is_shadow = False
        self.bar_length = bar_length
        if '|' not in line:
            raise ParseError(line_number, 'malformed music line (no bars)')
        bars = line.split('|')[:-1] # ignore whatever's after the final bar
        if bars[0] != '':
            try:
                times = int(bars[0])
            except ValueError:
                raise ParseError(line_number, 'malformed music line (times part)')
        else:
            times = 1
        bars = bars[1:] # drop times marker (if any)
        if set(map(len, bars)) != set([bar_length]):
            raise ParseError(line_number, 'malformed music line (wrong bar length)')
        self.code = ''.join(bars) * times
        
    def __len__(self):
        return len(self.code)/self.bar_length

INSTRUMENTS = {
    'low': ['surdo_l'],
    'mid': ['surdo_m'],
    'high': ['surdo_h'],
    'low surdo': ['surdo_l'],
    'mid surdo': ['surdo_m'],
    'high surdo': ['surdo_h'],
    'surdos': ['surdo_l', 'surdo_m', 'surdo_h'],
    'repenique': ['rep'],
    'snare': ['snare'],
    'tamborim': ['tamborin'],
    'agogo': ['agogo'],
    'shaker': ['shaker'],
    'all': ['surdo_l', 'surdo_m', 'surdo_h', 'rep', 'snare', 'tamborin', 'agogo', 'shaker']
}
ALL_OTHERS = ['all others', 'others']
EVERYONE = ['everyone', 'everybody']

# '' is default sample, used in breaks
INSTRUMENT_SAMPLES = {
    # missing: surdo dampened
    'surdo_l': { '': 'X', 's': 'sil', 'o': 'X', 'x': 'X' },
    'surdo_m': { '': 'X', 's': 'sil', 'o': 'X', 'x': 'X' },
    'surdo_h': { '': 'X', 's': 'sil', 'o': 'X', 'x': 'X' },
    # missing (?): silent
    'rep': { '': 'X', 'f': 'flare', 'h': 'hand', 'r': 'rim', 's': None, 'x': 'X' },
    # N.B. 'sh' really has two variants!
    'snare': { '': 'X', '.': 'sh', 'x': 'X' },
    # sample '' means do not adjoin '_'
    'tamborin': { '': '', 'x': '' },
    'agogo': {'': 'high', 'o': 'low', 'l': 'low', 'h': 'high' },
    # shaker does not play in breaks!
    # also, shaker lasts 16 beats and is only good for 120rpm!
    'shaker': {'': None, 'x': 'bar'}
}

class Entry:
    "Entry in a tune file"
    def __init__(self, bar_length, name, text, groove = None):
        "Parse an entry, given as a collection of (line_number, text) pairs"
        self.bar_length = bar_length
        self.name = name
        # read length
        signature = text[0]
        psignature = word_split(signature[1])
        if psignature[0].lower() != 'length':
            raise ParseError(signature[0], 'expected entry length')
        try:
            self.length = int(psignature[1])
        except ValueError:
            raise ParseError(signature[0], 'malformed entry length')
        # read entry type
        key = text[1]
        if key[1].lower() == 'instruments':
            self.type = 'Groove'
        elif key[1].lower() == 'notation':
            self.type = 'Break'
        else:
            raise ParseError(key[0], 'unknown entry type')
        # reset tweaks
        self.tweaks = []
        # read instrumentation/notation
        prelude = []
        music = []
        music_reached = False
        for line_number, line in text[2:]:
            if music_reached:
                music.append((line_number, line))
            else:
                if line.lower() == 'music':
                    music_reached = True
                else:
                    prelude.append((line_number, line))
        if len(music) == 0:
            raise ParseError(text[-1][0], 'entry without music')
        ## Groove
        if self.type == 'Groove':
            # parse instrumentation
            self.parse_instruments(prelude)
            # parse music
            self.music = dict([])
            inst_ix = 0 # index of current instrument
            code = ''
            for line_number, line in music:
                if code == '':
                    starting_line = line_number
                code += Line(bar_length, line_number, line).code
                if len(code) > bar_length * self.length:
                    raise ParseError(line_number, 'music for %s too long' % prelude[inst_ix])
                if len(code) == bar_length * self.length:
                    for inst in self.instruments[inst_ix]:
                        self.music[inst] = self.parse_groove_line(starting_line, inst, code)
                    inst_ix += 1
                    code = ''
            if inst_ix != len(self.instruments):
                raise ParseError(music[-1][0], 'not enough music')
            self.tlength = self.length
        ## Break
        else:
            self.instruments = groove.music.keys()
            self.groove_length = groove.length
            # parse instrumentation
            self.parse_notation(prelude)
            # parse music
            codes = []
            code = ''
            for line_number, line in music:
                if line == '*':
                    if len(code) < bar_length * self.length:
                        raise ParseError(line_number, 'music for %s too short')
                    codes.append(code)
                    code = ''
                else:
                    code += Line(bar_length, line_number, line).code
                    if len(code) > bar_length * self.length:
                        raise ParseError(line_number, 'music too long')
            if len(code) < bar_length * self.length:
                raise ParseError(music[-1][0], 'not enough music')
            codes.append(code)
            # enter music
            self.music = dict([(instrument, []) for instrument in self.instruments])
            for code in codes:
                for instrument in self.instruments:
                    if self.groove_continues:
                        if instrument in self.special_insts:
                            self.music[instrument] += self.parse_break_line(music[0][0], instrument, code)
                        else:
                            self.music[instrument] = self.repeat(self.length, groove.music[instrument])
                    else:
                        self.music[instrument] += self.parse_break_line(music[0][0], instrument, code)
            self.tlength = self.length + self.delay
        # check tweaks
        self.apply_tweaks()
    def __repr__(self):
        return '<%s: %s>' % (self.type, self.name)
    def parse_instrument(self, line_number, line):
        "Parse a line in an instrument list"
        res = set([])
        for name0 in line.split('+'):
            name = name0.strip().lower()
            if name in ALL_OTHERS:
                res.update(['all_others'])
            elif name in EVERYONE:
                res.update(['everyone'])
            else:
                if name not in INSTRUMENTS:
                    raise ParseError(line_number, 'unknown instrument %s' % name)
                res.update(INSTRUMENTS[name])
        return res
    def parse_instruments(self, text):
        "Parse an instrument list into instruments"
        self.instruments = []
        for line_number, line in text:
            self.instruments.append(self.parse_instrument(line_number, line))
    def parse_groove_line(self, starting_line, instrument, code):
        "Parse a groove line of music"
        res = []
        for t, c0 in enumerate(code):
            if c0 == ' ':
                continue
            c = c0.lower()
            if c not in INSTRUMENT_SAMPLES[instrument]:
                raise ParseError(starting_line, 'unknown code "%s" for %s' % (c0, instrument))
            patch = INSTRUMENT_SAMPLES[instrument][c]
            if patch is not None:
                # dampen?
                if c == 'o' and patch == 'x':
                    mag = 0.5
                else:
                    mag = 1
                res.append((t, patch, mag))
        return res
    def parse_notation(self, text):
        "Parse notation segment into notation, special_insts, groove_continues"
        self.notation = dict([])
        self.notation_type = dict([])
        self.special_insts = set([])
        self.groove_continues = True
        self.delay = 0
        for line_number, line in text:
            pline = word_split(line)
            if pline[0].lower() == 'delay':
                if len(pline) == 1:
                    raise ParseError(line_number, 'missing delay')
                try:
                    self.delay = int(pline[1])
                except ValueError:
                    raise ParseError(line_number, 'malformed delay')
                continue
            if line.lower() == 'soft to loud':
                self.tweaks.append((line_number, line))
                continue
            pline = [part.strip() for part in line.split('=')]
            pline[0] = pline[0].lower()
            if len(pline) < 2 or len(pline) > 3 or len(pline[0]) != 1:
                raise ParseError(line_number, 'malformed notation line')
            if pline[0] in self.notation:
                raise ParseError(line_number, 'notation "%s" already defined' % pline[0])
            insts = self.parse_instrument(line_number, pline[1])
            self.notation[pline[0]] = insts
            if len(pline) >= 3:
                if len(pline[2]) != 1:
                    raise ParseError(line_number, 'malformed notation line')
                self.notation_type[pline[0]] = pline[2]
            else:
                self.notation_type[pline[0]] = ''
            self.special_insts.update(insts)
        for special in ['all_others', 'everyone']:
            if special in self.special_insts:
                self.special_insts.remove(special)
                self.groove_continues = False
        if not self.groove_continues:
            everyone = set(self.instruments)
            all_others = everyone.difference(self.special_insts)
            for c in self.notation:
                insts = self.notation[c]
                for name, ilist in [('all_others', all_others), ('everyone', everyone)]:
                    if name in insts:
                        insts.remove(name)
                        insts.update(ilist)
    def parse_break_line(self, starting_line, instrument, code):
        "Parse a break line of music"
        res = []
        for t, c0 in enumerate(code):
            if c0 == ' ':
                continue
            c = c0.lower()
            if c not in self.notation:
                raise ParseError(starting_line, 'unknown notation "%s"' % c0)
            if instrument in self.notation[c]:
                patch = INSTRUMENT_SAMPLES[instrument][self.notation_type[c]]
                if patch is not None:
                    res.append((t + self.delay * self.bar_length, patch, 1))
        return res
    def repeat(self, bars, line):
        "Repeat a groove instrumental line for set number of bars"
        res = []
        for repeat in range(int(float(bars)/self.groove_length + 0.999)):
            t0 = self.bar_length * self.groove_length * repeat
            res += [(t0 + t, type, mag) for (t, type, mag) in line if t0 + t < bars * self.bar_length]
        return res
    def apply_tweaks(self):
        "Apply tweaks"
        for line_number, tweak in self.tweaks:
            if tweak == 'soft to loud':
                if self.bar_length != 4:
                    raise ParseError(line_number, 'soft to loud only supported on 4 x c')
                for instrument in self.music:
                    new_notes = []
                    for t, type, volume in self.music[instrument]:
                        mult = [1./3, 2./3, 1., 2.][t/self.length]
                        new_notes.append((t, type, volume * mult))
                    self.music[instrument] = new_notes

class Tune:
    "Tune file"
    def __init__(self, filename):
        text = file(filename, 'r').read().splitlines()
        text = [line.strip() for line in text]
        text = [(num+1, line) for (num, line) in enumerate(text) if line != '' and line[0] != '#']
        # read tune name
        header = text[0]
        pheader = word_split(header[1])
        if pheader[0].lower() != 'tune':
            raise ParseError(header[0], 'expected tune header')
        self.name = ' '.join(pheader[1:])
        # read time signature
        signature = text[1]
        psignature = word_split(signature[1])
        if psignature[0].lower() != 'bar':
            raise ParseError(signature[0], 'expected time signature')
        try:
            self.bar = int(psignature[1])
        except ValueError:
            raise ParseError(signature[0], 'malformed time signature')
        # read entries
        self.entries = dict([])
        entry = []
        entry_name = ''
        end_encountered = False
        for line_number, line in text[2:]:
            keyword = line.split(' ')[0].lower()
            if keyword == 'end' or keyword == 'entry':
                if entry_name != '':
                    if len(self.entries) == 0:
                        groove = Entry(self.bar, entry_name, entry)
                        if groove.type != 'Groove':
                            raise ParseError(entry[0][0], 'first entry should be the groove')
                        self.entries[entry_name] = groove
                    else:
                        self.entries[entry_name] = Entry(self.bar, entry_name, entry, groove)
                if keyword == 'end':
                    end_encountered = True
                    break
                else:
                    entry = []
                    entry_name = ' '.join(line.split(' ')[1:])
                    if entry_name in self.entries:
                        raise ParseError(line_number, 'duplicate entry %s' % entry_name)
            else:
                entry.append((line_number, line))
        if not end_encountered:
            raise ParseError(text[-1][0], 'missing end statement')
    def __repr__(self):
        return '<Tune: %s>' % self.name
    
